﻿using System;
using System.Collections.Generic;
using System.Text;
using FlexminAPI;
using FlexLib;
using System.IO;
using System.Xml;
using ICSharpCode.SharpZipLib.Zip;
using System.Collections;
using System.Security;
using System.Windows.Forms;

namespace FlexLib.Plugins
{
	/// <summary>
	/// Менеджер плагинов.
	/// Управляет установкой и удалением плагинов,
	/// предоставляет список уже установленных.
	/// </summary>
	public class PluginManager
	{
		static private PluginManager adminInstance = null;
		static private PluginManager hostInstance = null;

		private enum ManagerType
		{
			Admin,
			Host
		}

		static public PluginManager AdminInstance
		{
			get
			{
				if (adminInstance == null)
					adminInstance = new PluginManager(ManagerType.Admin);

				return adminInstance;
			}
		}

		static public PluginManager HostInstance
		{
			get
			{
				if (hostInstance == null)
					hostInstance = new PluginManager(ManagerType.Host);

				return hostInstance;
			}
		}

		public event EventHandler Change;

		private ManagerType type;

//		private FileSystemWatcher watcher;

		private PluginManager(ManagerType type)
		{
			this.type = type;

			pluginXmlPath = new Dictionary<string, string>();

			plugins = new Dictionary<string, Plugin>();

			LoadPluginsList();

			LoadAllPlugins();
/*
			try
			{
				watcher = new FileSystemWatcher(Path.GetDirectoryName(GetModulesListPath()));
				watcher.Changed += new FileSystemEventHandler(directory_Changed);
				watcher.Created += new FileSystemEventHandler(directory_Changed);
				watcher.Deleted += new FileSystemEventHandler(directory_Changed);
				watcher.EnableRaisingEvents = true;
			}
			catch { }
*/		}

		void directory_Changed(object sender, FileSystemEventArgs e)
		{
			if (e.FullPath != GetModulesListPath())
				return;

			try
			{
				LoadPluginsList();
			}
			catch
			{
				return;
			}

			foreach (string pluginName in pluginXmlPath.Keys)
			{
				if (plugins.ContainsKey(pluginName))
					continue;

				try
				{
					Plugin plugin = LoadPlugin(pluginName);
					plugins.Add(pluginName, plugin);
				}
				catch
				{
					// TODO: лог ошибки запуска плагина
				}
			}

			List<string> toRemove = new List<string>();
			foreach (string pluginName in plugins.Keys)
			{
				if (pluginXmlPath.ContainsKey(pluginName))
					continue;

				toRemove.Add(pluginName);
			}

			foreach (string pluginName in toRemove)
			{
				plugins[pluginName].Unload();
				plugins.Remove(pluginName);
			}

			if (Change != null)
				Change(this, new EventArgs());
		}

		private string GetModulesListPath()
		{
			string xmlName = "";

			switch (type)
			{
				case ManagerType.Admin:
					xmlName = "modules_admin.xml";
					break;
				case ManagerType.Host:
					xmlName = "modules_host.xml";
					break;
			}

			string path = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
			path += Path.DirectorySeparatorChar + "Flexmin" + Path.DirectorySeparatorChar + xmlName;

			return path;
		}

		public string GetModulePath(string name)
		{
			string path = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) +
					Path.DirectorySeparatorChar + "Flexmin" +
					Path.DirectorySeparatorChar + "Modules" +
					Path.DirectorySeparatorChar + name;

			return path;
		}

		private void LoadPluginsList()
		{
			string path = GetModulesListPath();

			if (!File.Exists(path))
				return;

			pluginXmlPath.Clear();

			XmlDocument modulesDoc = new XmlDocument();

			try
			{
				modulesDoc.Load(path);
			}
			catch (Exception ex)
			{
				throw new PluginException("Can't open plugin's configuration file or it has wrong format.", ex);
			}

			XmlNode rootNode = null;
			foreach (XmlNode subNode in modulesDoc.ChildNodes)
			{
				if (subNode.Name == "ModulesList")
				{
					rootNode = subNode;
					break;
				}
			}

			if (rootNode == null)
				throw new PluginException("Plugin's configuration file is damaged or has invalid format.");

			foreach (XmlNode node in rootNode.ChildNodes)
			{
				if (node.Name == "module")
				{
					try
					{
						string name = node.Attributes["name"].Value;
						string xml = node.Attributes["xml"].Value;

						pluginXmlPath.Add(name, xml);
					}
					catch
					{
						continue;
					}
				}
			}
		}

		private Dictionary<string, string> pluginXmlPath;

		private Dictionary<string, Plugin> plugins;

		public PluginInfo GetPluginInfo(string pluginName)
		{
			// TODO: обработка ошибок
			string xmlPath;
			if (!pluginXmlPath.TryGetValue(pluginName, out xmlPath))
				throw new PluginException("No such plugin.");

			XmlDocument pluginDoc = new XmlDocument();
			try
			{
				pluginDoc.Load(xmlPath);
			}
			catch (Exception ex)
			{
				throw new PluginException("Can't open plugin's configuration file or it has wrong format.", ex);
			}

			XmlNode rootNode = null;
			foreach (XmlNode subNode in pluginDoc.ChildNodes)
			{
				if (subNode.Name == "FlexminModule")
				{
					rootNode = subNode;
					break;
				}
			}

			if (rootNode == null)
				throw new PluginException("Root node not found");

			foreach (XmlNode groupNode in rootNode.ChildNodes)
			{
				switch (groupNode.Name)
				{
					case "info":
						return XmlPluginSerialization.ParseInfo(groupNode);
				}
			}

			throw new PluginException("Plugin info not found");
		}

		public IEnumerable<PluginInfo> InstalledPlugins
		{
			get
			{
				List<PluginInfo> info = new List<PluginInfo>();

				foreach (string pluginName in pluginXmlPath.Keys)
				{
					PluginInfo pluginInfo;
					try
					{
						pluginInfo = GetPluginInfo(pluginName);
					}
					catch (PluginException)
					{
						continue;
					}

					info.Add(pluginInfo);
				}

				return info;
			}
		}

		// TODO: коллекция позволяет менять содержимое
		//		 здесь это недопустимо
		public ICollection<Plugin> Plugins
		{
			get
			{
				return plugins.Values;
			}
		}

		public Plugin LoadPlugin(string name)
		{
			// TODO: обработка ошибок
			string xmlPath = pluginXmlPath[name];
			XmlDocument pluginDoc = new XmlDocument();

			try
			{
				pluginDoc.Load(xmlPath);
			}
			catch (Exception ex)
			{
				throw new PluginException("Can't open plugin's configuration file or it has wrong format.", ex);
			}

			XmlNode rootNode = null;
			foreach (XmlNode subNode in pluginDoc.ChildNodes)
			{
				if (subNode.Name == "FlexminModule")
				{
					rootNode = subNode;
					break;
				}
			}

			if (rootNode == null)
				throw new PluginException("Root node not found");

			PluginInfo info = null;
			string assemblyPath = null;

			foreach (XmlNode groupNode in rootNode.ChildNodes)
			{
				switch (groupNode.Name)
				{
					case "assembly":
						assemblyPath = groupNode.InnerText;
						break;
					case "info":
						info = XmlPluginSerialization.ParseInfo(groupNode);
						break;
				}
			}

			if (info == null || assemblyPath == null)
			{
				throw new PluginException("Plugin's configuration file is damaged or has invalid format.");
			}

			return new Plugin(info, GetFullPath(assemblyPath, info));
		}

		private void LoadAllPlugins()
		{
			foreach (string pluginName in pluginXmlPath.Keys)
			{
				Plugin plugin;
				try
				{
					plugin = LoadPlugin(pluginName);
				}
				catch (Exception ex)
				{
					continue;
				}

				plugins.Add(pluginName, plugin);
			}
		}

		const int MAX_COPY_SIZE = 8192;

		private string GetFullPath(string virtualPath, PluginInfo info)
		{
			if (virtualPath.Contains(":"))
				return virtualPath;

			string[] virtualPathParts = virtualPath.Split(new char[] { '\\', '/' });

			string virtualDir = virtualPathParts[0];
			string fileName = virtualPathParts[virtualPathParts.Length - 1];
			string subPath = virtualPath.Substring(0, virtualPath.Length - fileName.Length);
			subPath = subPath.Substring(virtualDir.Length, subPath.Length - virtualDir.Length);
			subPath = subPath.Replace('/', '\\');

			string fileDir = "";
			switch (virtualDir)
			{
				case "Plugin directory":
					fileDir = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) +
								Path.DirectorySeparatorChar + "Flexmin" +
								Path.DirectorySeparatorChar + "Modules" +
								Path.DirectorySeparatorChar + info.Name;
					break;

				case "Common directory":
					fileDir = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) +
								Path.DirectorySeparatorChar + "Flexmin" +
								Path.DirectorySeparatorChar + "Modules" +
								Path.DirectorySeparatorChar + "Common";
					break;

				case "Windows directory":
					fileDir = Environment.GetEnvironmentVariable("windir");
					break;

				case "System directory":
					fileDir = Environment.GetFolderPath(Environment.SpecialFolder.System);
					break;

				case "Fonts directory":
					// TODO: Шрифты надо не просто копировать, а устанавливать...
					break;
			}

			fileDir += subPath;

			Directory.CreateDirectory(fileDir);

			// WARNING! тестовая заглушка
			return Path.GetDirectoryName(Application.ExecutablePath) +
				Path.DirectorySeparatorChar +
				fileName;

			return fileDir + fileName;
		}

		/// <summary>
		/// Установка файла
		/// </summary>
		/// <param name="virtualPath">Виртуальный путь файлв</param>
		/// <param name="stream">Поток с содержимым файла</param>
		/// <returns>Путь, по которому установлен файл</returns>
		private string InstallFile(string virtualPath, Stream stream, PluginInfo info)
		{
			string fullPath = GetFullPath(virtualPath, info);

			FileStream file;
			try
			{
				file = new FileStream(fullPath, FileMode.Create);
			}
			catch
			{
				return fullPath;
			}

			byte[] buffer = new byte[MAX_COPY_SIZE];
			int numBytes;
			while ((numBytes = stream.Read(buffer, 0, buffer.Length)) > 0)
				file.Write(buffer, 0, numBytes);
			file.Close();

			return fullPath;
		}

		public void Install(string path)
		{
			Package packageProperties = new Package(path);

			if (pluginXmlPath.ContainsKey(packageProperties.Info.Name))
				throw new PluginAlreadyInstalledException("Plugin already installed");

			string pluginDir = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
			pluginDir += Path.DirectorySeparatorChar + "Flexmin" +
						Path.DirectorySeparatorChar + "Modules" +
						Path.DirectorySeparatorChar + packageProperties.Info.Name;

			Directory.CreateDirectory(pluginDir);

			string modulePropertiesPath = pluginDir + Path.DirectorySeparatorChar;
			switch (type)
			{
				case ManagerType.Admin:
					modulePropertiesPath += "module_admin.xml";
					break;
				case ManagerType.Host:
					modulePropertiesPath += "module_host.xml";
					break;
			}

			XmlTextWriter xmlWriter = new XmlTextWriter(modulePropertiesPath, Encoding.UTF8);
			xmlWriter.Formatting = Formatting.Indented;

			xmlWriter.WriteStartDocument();
			xmlWriter.WriteStartElement("FlexminModule");

			string assemblyPath = "";

			switch (type)
			{
				case ManagerType.Admin:
					assemblyPath = packageProperties.AssemblyPaths.MasterPath;
					break;
				case ManagerType.Host:
					assemblyPath = packageProperties.AssemblyPaths.SlavePath;
					break;
			}

			xmlWriter.WriteStartElement("assembly");
			xmlWriter.WriteString(assemblyPath);
			xmlWriter.WriteEndElement();

			XmlPluginSerialization.SerializePluginInfo(xmlWriter, packageProperties.Info);

			xmlWriter.WriteStartElement("files");
			ZipFile package = new ZipFile(path);
			string prefix = "";
			switch (type)
			{
				case ManagerType.Admin:
					prefix = "admin\\";
					break;
				case ManagerType.Host:
					prefix = "user\\";
					break;
			}
				
			foreach (ZipEntry entry in package)
			{
				if (!entry.IsFile)
					continue;

				if (entry.Name == "module.xml")
					continue;

				if (!entry.Name.StartsWith(prefix, StringComparison.InvariantCultureIgnoreCase))
					continue;

				Stream stream = package.GetInputStream(entry);
				string fileVirtualPath = entry.Name.Substring(prefix.Length, entry.Name.Length - prefix.Length);
				string filePath = InstallFile(fileVirtualPath,
												stream,
												packageProperties.Info);
				stream.Close();

				xmlWriter.WriteStartElement("file");
				xmlWriter.WriteString(fileVirtualPath);
				xmlWriter.WriteEndElement();

			}

			xmlWriter.WriteEndElement();
			xmlWriter.WriteEndElement();
			xmlWriter.Close();

			File.Copy(path, pluginDir + Path.DirectorySeparatorChar + packageProperties.Info.Name + ".fpkg", true);

			pluginXmlPath.Add(packageProperties.Info.Name, modulePropertiesPath);
			SavePluginsList();

			try
			{
				Plugin plugin = LoadPlugin(packageProperties.Info.Name);
				plugins.Add(packageProperties.Info.Name, plugin);
			}
			catch
			{
				// TODO: лог ошибки запуска плагина
			}

			if (Change != null)
				Change(this, new EventArgs());
		}

		private void SavePluginsList()
		{
			string path = GetModulesListPath();

			XmlTextWriter xmlWriter = new XmlTextWriter(path, Encoding.UTF8);
			xmlWriter.Formatting = Formatting.Indented;
			xmlWriter.WriteStartDocument();
			xmlWriter.WriteStartElement("ModulesList");

			foreach (string key in pluginXmlPath.Keys)
			{
				xmlWriter.WriteStartElement("module");
				xmlWriter.WriteAttributeString("name", key);
				xmlWriter.WriteAttributeString("xml", pluginXmlPath[key]);
				xmlWriter.WriteEndElement();
			}

			xmlWriter.WriteEndElement();
			xmlWriter.WriteEndDocument();
			xmlWriter.Close();
		}

		public void Upgrade(string path)
		{
			if (Change != null)
				Change(this, new EventArgs());
		}

		private List<string> GetInstalledFiles(string xmlPath)
		{
			List<string> installedFiles = new List<string>();

			XmlDocument pluginDoc = new XmlDocument();

			try
			{
				pluginDoc.Load(xmlPath);
			}
			catch (Exception ex)
			{
				throw new PluginException("Can't open plugin's configuration file or it has wrong format.", ex);
			}


			XmlNode rootNode = null;
			foreach (XmlNode subNode in pluginDoc.ChildNodes)
			{
				if (subNode.Name == "FlexminModule")
				{
					rootNode = subNode;
					break;
				}
			}

			if (rootNode == null)
				throw new PluginException("Root node not found");

			PluginInfo info = null;
			foreach (XmlNode groupNode in rootNode.ChildNodes)
			{
				switch (groupNode.Name)
				{
					case "assembly":
						installedFiles.Add(groupNode.InnerText);
						break;

					case "info":
						info = XmlPluginSerialization.ParseInfo(groupNode);
						break;

					case "files":
						foreach (XmlNode fileNode in groupNode.ChildNodes)
						{
							if (fileNode.Name == "file")
							{
								installedFiles.Add(fileNode.InnerText);
							}
						}
						break;
				}
			}

			List<string> fullFilePaths = new List<string>();
			foreach (string file in installedFiles)
			{
				fullFilePaths.Add(GetFullPath(file, info));
			}

			return fullFilePaths;
		}

		public void Uninstall(string name)
		{
			if (!pluginXmlPath.ContainsKey(name))
				throw new PluginException("Plugin does not exist.");

			List<string> filesToDelete = new List<string>();

			string xmlPath = pluginXmlPath[name];

			filesToDelete = GetInstalledFiles(xmlPath);

			filesToDelete.Add(xmlPath);

			string moduleDir = GetModulePath(name);

			string packagePath = moduleDir + Path.DirectorySeparatorChar + name + ".fpkg";

			filesToDelete.Add(packagePath);

			List<string> lockedFiles;

			PluginManager otherManager = null;

			switch (type)
			{
				case ManagerType.Admin:
					otherManager = PluginManager.HostInstance;
					break;

				case ManagerType.Host:
					otherManager = PluginManager.AdminInstance;
					break;
			}

			if (otherManager == null)
				throw new PluginException("Can't get plugin manager instance.");

			if (otherManager.pluginXmlPath.ContainsKey(name))
			{
				lockedFiles = otherManager.GetInstalledFiles(otherManager.pluginXmlPath[name]);
				lockedFiles.Add(packagePath);
			}
			else
			{
				lockedFiles = new List<string>();
			}

			foreach (string path in filesToDelete)
			{
				if (lockedFiles.Contains(path))
					continue;

				try
				{
					File.Delete(path);

					string subPath = Path.GetDirectoryName(path);

					subPath = Directory.GetParent(subPath).FullName;

					while (true)
					{
						try
						{
							Directory.Delete(subPath);
							subPath = Directory.GetParent(subPath).FullName;
						}
						catch
						{
							break;
						}
					}
				}
				catch
				{
					continue;
				}
			}

			pluginXmlPath.Remove(name);

			if (plugins.ContainsKey(name))
			{
				Plugin plugin = plugins[name];
				plugin.Unload();
				plugins.Remove(name);
			}

			SavePluginsList();

			if (Change != null)
				Change(this, new EventArgs());
		}

		public static void UninstallPlugin(SocketReader reader, SocketWriter writer)
		{
			try
			{
				string pluginName = reader.ReadString();

				try
				{
					PluginManager.HostInstance.Uninstall(pluginName);
				}
				catch (PluginException ex)
				{
					return;
				}

			}
			catch (Exception ex)
			{
			}
		}
	}
}
